Вторая и третья часть задания: извлечение feature vector и применение градиентного бустинга¶

Начнем с извлечения feature vector¶

Загрузка библиотек:

In [1]:
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
# import torch.optim as optim
#import os
# import torch.nn.functional as F
import matplotlib.pyplot as plt
# from collections import defaultdict # to check distribution by classes
# from sklearn.metrics import precision_recall_fscore_support # to calculate F1 score
# from sklearn.model_selection import StratifiedShuffleSplit # to split images to train, val, and test
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\tqdm\auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Загружаем лучший стейт обученной модели

In [2]:
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
image_dataset = torchvision.datasets.ImageFolder('../images', transform=transform)
image_dataloader = torch.utils.data.DataLoader(image_dataset, batch_size=32, shuffle=False)

model = torchvision.models.resnet18(weights='ResNet18_Weights.IMAGENET1K_V1')
model.fc = nn.Linear(512, len(image_dataset.classes))
model.load_state_dict(torch.load('../data/best_model_42_epoch_01_last.pt'))
image_classes = image_dataset.classes

Заменяем последний полносвязный уровень на единицы, чтобы извлечь признаки предпоследнего уровня для каждого изображения

In [3]:
model.fc_backup = model.fc
# model.fc = nn.Sequential()
model.fc = nn.Identity()
In [4]:
model.eval()
all_features = []
with torch.no_grad():
    for inputs, labels in image_dataloader:
        outputs = model(inputs)
        features = outputs
        all_features.append(features)
all_features = torch.cat(all_features, dim=0)
In [5]:
print(all_features)
print(all_features.size())
labels = np.concatenate([batch[1].numpy() for batch in image_dataloader])
tensor([[1.3572, 1.6053, 0.1278,  ..., 2.3075, 0.6772, 3.0499],
        [6.0935, 2.7589, 0.0000,  ..., 3.3483, 4.4636, 0.0000],
        [0.7585, 0.0000, 0.5369,  ..., 0.4460, 0.0000, 0.0000],
        ...,
        [0.0000, 0.0000, 1.3359,  ..., 0.0670, 2.0176, 0.0000],
        [0.0000, 1.7225, 2.1514,  ..., 0.0000, 0.0000, 1.4539],
        [0.0422, 2.4869, 1.3453,  ..., 0.8349, 0.2712, 0.0000]])
torch.Size([1422, 512])

Полученная матрица (тензор) feature vector имеет размерность количество изображений (1422) * количество нейронов (512) на последнем уровне.

Попробуем сделать кластеризацию различными алгоритмами¶

Сначала попробуем понизить размерность: используем PCA.

In [6]:
# используем StandardScaler для шкалирования наших данных
sc = StandardScaler()
X_scaled = sc.fit_transform(all_features)

# Apply PCA
pca = PCA(n_components=None)
pca.fit(X_scaled)

# Get the eigenvalues
# print("Eigenvalues:")
# print(pca.explained_variance_)
# print()

# Get explained variances
# print("Variances (Percentage):")
# print(pca.explained_variance_ratio_ * 100)
# print()

# Make the scree plot
plt.plot(np.cumsum(pca.explained_variance_ratio_ * 100))
plt.xlabel("Number of components (Dimensions)")
plt.ylabel("Explained variance (%)")
plt.hlines(y = 60, xmin = 0, xmax = 512, colors = 'g', linestyles = '--')
plt.show()

Можно увидеть, что первые главные компоненты объясняют достаточно мало дисперсии. Это значит, что признаки получились мало коррелированы между собой. Попробуем сделать кластеризацию методом к-средних на исходной матрице, количество кластеров выбираем 8, как и число классов изображения:

In [7]:
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=8, random_state=42, n_init=10)
kmeans.fit(all_features)
labels_kmeans = kmeans.labels_
centers = kmeans.cluster_centers_

from sklearn.metrics import adjusted_rand_score

labels == labels_kmeans
ari = adjusted_rand_score(labels, labels_kmeans)
print("Adjusted Rand Index: {:.4f}".format(ari))
Adjusted Rand Index: 0.1536

Для оценки точности используем функцию adjusted_rand_score, которая сама определяет наиболее вероятный лейбл кластера. Точность кластеризации получилась не очень высокая. Для визуализации и отрисовки лейблов используем UMAP.

In [8]:
import umap.umap_ as umap
embedding = umap.UMAP(n_neighbors=15,
                      min_dist=0.3,
                      metric='correlation').fit_transform(all_features)
In [9]:
# Plot the UMAP visualization with true labels

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
# axes[1].colorbar()
# plt.colorbar(ax=axes)
fig.suptitle('k-means based on the original matrix, ARI = {:.4f}'.format(ari))

plt.show()

# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with true labels')
# plt.show()

# # Plot the UMAP visualization with predicted labels
# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with predicted labels')
# plt.show()

Попробуем использовать алгоритм к-средних на матрице из первых 100 главных компонент

In [10]:
# from sklearn.decomposition import PCA

pca = PCA(n_components=100)
pca_data = pca.fit_transform(all_features)

# Perform k-means clustering on the PCA components
kmeans_pca = KMeans(n_clusters=8, random_state=42, n_init=10)
kmeans_pca.fit(pca_data)

# Print the cluster labels assigned to each data point
print(kmeans_pca.labels_)

ari_pca = adjusted_rand_score(labels, kmeans_pca.labels_)
print("Adjusted Rand Index: {:.4f}".format(ari_pca))
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=kmeans_pca.labels_, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
fig.suptitle('k-means based on first 100 PC, ARI = {:.4f}'.format(ari_pca))

# axes[1].colorbar()
# plt.colorbar(ax=axes)
plt.show()
[3 4 3 ... 4 2 3]
Adjusted Rand Index: 0.1516

Точность кластеризации не улучшилась после использования главных компонент, возможно потому что исходные переменные не были сильно скоррелированы друг с другом. Попробуем использовать иерархическую кластеризацию

In [11]:
# import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster

# Perform hierarchical clustering
Z = linkage(all_features, method='ward')

# Cut the dendrogram to 8 groups
labels_hier = fcluster(Z, 8, criterion='maxclust')
color_threshold = Z[-7, 2]
# Plot the dendrogram to visualize the hierarchy

dendrogram(Z, color_threshold=color_threshold)
plt.title('Dendrogram')
plt.xlabel('Sample index')
plt.ylabel('Distance')
plt.show()


# Compare the resulting labels with the true labels
score = adjusted_rand_score(labels, labels_hier)
print('Adjusted Rand score:', score)
Adjusted Rand score: 0.1625178276870089

Результат сопоставим с тем, что получилось алгоритмом k-means.

Теперь попробуем использовать метод k-ближайших соседей. Загрузим те же самые индексы тренировочного датасета и тестового + валидационного, чтобы избежать утечки данных.

In [12]:
train_index = np.load('../data/train_index_42.npy')
# train_index
test_index = np.load('../data/test_index_42.npy')
# print(test_index)
X_train = all_features[train_index]
X_test = all_features[test_index]
y_train = labels[train_index]
y_test = labels[test_index]
In [13]:
from sklearn.neighbors import KNeighborsClassifier
# from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# k = 16  # сработало хорошо
k = 15
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)

y_pred = knn.predict(X_test)
report = classification_report(y_test, y_pred, zero_division=0)
print(report)

acc = np.mean(y_pred == y_test)
print("Classification accuracy: {:.2f}%".format(acc * 100))
              precision    recall  f1-score   support

           0       1.00      0.16      0.27        19
           1       0.55      0.92      0.69       110
           2       0.57      0.71      0.63        72
           3       0.76      0.21      0.33        61
           4       0.57      0.66      0.61        65
           5       0.69      0.69      0.69        35
           6       1.00      0.22      0.36        23
           7       0.72      0.31      0.43        42

    accuracy                           0.59       427
   macro avg       0.73      0.48      0.50       427
weighted avg       0.66      0.59      0.55       427

Classification accuracy: 59.25%

Метод k-ближайших соседей отработал лучше, чем k-means, поэтому попробуем визуализировать картинки с соответствующим лейблом. По очереди отрисуем картинки, соответствующие предсказанным лейблам. Картинок в классах разное количество, поэтому по возможности будут отрисованы все, но максимум 36 изображений хорошо размещаются на одном полотне.

In [14]:
def show_images(indices, nrow=4,ncol=4, title = ''):
    images = [image_dataset[idx][0] for idx in indices]
    curr_labels = labels[indices]
    curr_labels = (np.take(image_classes, curr_labels))
    fig, axs = plt.subplots(nrows=nrow, ncols=ncol, figsize=(21, 12))
#     plt.title('title')

# iterate over the subplots and plot the images and labels
    for i, ax in enumerate(axs.flat):
        # plot the image
        imgs = images[i].numpy().transpose((1, 2, 0))
        mean = np.array([0.485, 0.456, 0.406])
        std = np.array([0.229, 0.224, 0.225])
        imgs = std * imgs + mean
        imgs = np.clip(imgs, 0, 1)
        ax.imshow(imgs)
        ax.set_title(str(curr_labels[i]))
        ax.axis("off")
    fig.suptitle(title, fontsize=18)
    plt.show()
In [15]:
print(test_index[y_pred == 0])
show_images(test_index[y_pred == 0], 1, len(test_index[y_pred == 0]), 'Для артдеко характерно наличие картинки по центру, по краям фон')
[52 28 23]

Первый класс изображений - предсказанный алгоритмом к-ближайших соседей стиль АртДеко. Определилось три изображения, все относятся к нужному классу. Можно отметить наличие изображения по центру, в то время как по краям фон. Возможно, это являлось признаком, на которые обращает внимание модель. Отрисуем следующий класс изображений: кубизм

In [16]:
show_images(test_index[y_pred == 1], 6, 6, 'Для кубизма сложно найти ярко выраженные фичи')

Видно, что многие изображения определились правильно, однако очень много ложно-позитивных срабатываний. В отличие от предыдущего случая, здесь изображения заполнены содержанием по всей площади. Цвета яркие. Я смотрела сама изображения в папке кубизм, туда попал в том числе Сезанн, который скорее в стиле постимпрессионизма пишет. Например, здесь это изображение во второй строке четвертого столбца (paul-cezanne_chateau-noir-1). Подобные изображения могли затруднить идентификацию признаков изображений классического кубизма (Малевич). Перейдем к импрессионизму

In [17]:
print(len(test_index[y_pred == 2]))
show_images(test_index[y_pred == 2], 6, 6, 'Для импрессионизма характерны изображения природы')
90

Особенности картин, которые классифицированы как импрессионизм ярко выделяются: изображения природы, деревьев, озер. По этой причине натурализм и фотографии, где изображена природа ошибочно классифицированы как импрессионизм. Например, изображение рококо в третьей строке 4 столбца, где изображены березы, на неискушенный глаз похоже на картину импрессиониста. Так что неудивительно, что нейросеть тоже ошиблась :)

In [18]:
print(len(test_index[y_pred == 3]))
show_images(test_index[y_pred == 3], 4, 4, 'Для японизма возможно ключевым признаком является бежевый фон')
17

Кажется что нейросеть определелиа для японизма характерным свойством бежевый светлый фон, возможно как в той истории про то как волков от собак отличали по снежному фону у волков. Это объясняет почему сюда попал кубизм (2 строка 3 столбец), птичка (3, 3) и розовая бутылка (3, 2)

In [19]:
show_images(test_index[y_pred == 4], 6, 6, 'В натурализме много растений, в особенности цветов')

Для натурализма характерны такие формы как цветы и листья. Есть несколько странных ложнопозитивных срабатываний, например машина или кот на ковре, что сложно интерпретировать. Однако в остальном можно увидеть общие черты изображений. Аисты (2 строка 2 столбец) похожи скорее на изображение природы, чем то что нейросеть воспринимает как рококо, подробнее ниже ->

In [20]:
show_images(test_index[y_pred == 5], 6, 5, 'Большинство изображений стиля рококо содержат лица')

Одно из самых ярких кластеров признаков - Рококо. Почти на всех изображениях есть человек, а точнее лицо. Это объясняет почему фотографии с лицами людей определились в рококо. Кроме того, любопытно что нейросеть распознает и группы людей, где лица некрупным планом. Можно сказать, что для нейросети определящим признаком, что изображение относится к рококо, является наличие людей.

In [21]:
show_images(test_index[y_pred == 6], 1, len(test_index[y_pred == 6]), 'Для комиксов (cartoon) характерны яркие цвета')

Картинок с мультиками немного, но они корректно определились. Характерным свойством является яркие, даже слишком цвета. К сожалению, выборка в исходном датасете небольшая, чтобы делать более конкретные выводы.

In [22]:
print(len(test_index[y_pred == 7],))
show_images(test_index[y_pred == 7], 3, 6, 'Для фотографий характерны изображения транспорта и коты')
18

Похоже для нейросети важным признаком фотографии являются машинки, мотициклы, в целом транспорт и коты. Некоторые изображения определились достаточно неочевидно, например артдеко и японизм, которых ложно записали сюда. Но в целом логика прослеживается.

Можно заметить, что извлеченные изображения достаточно контрастные, хотя кубизм оказался достаточно шумным, без ярко выраженных фичей. Возможно, стоит увеличить выборку и если есть возможность вручную переопределить некоторые спорные лейблы.

Третья часть задания: выполняем градиентный бустинг на извлеченные фичи¶

In [23]:
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix, f1_score

# Create a gradient boosting classifier
gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)

# Train the classifier on the training set
gb.fit(X_train, y_train)

# Make predictions on the test set
y_pred = gb.predict(X_test)
confusion_matrix_gb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_gb)

# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix:
[[ 3  8  0  6  2  0  0  0]
 [ 1 93  4  3  2  0  2  5]
 [ 0  8 50  2  9  3  0  0]
 [ 0 12  3 38  4  0  1  3]
 [ 2  6 11  4 35  2  1  4]
 [ 0  0  6  0  8 20  0  1]
 [ 0  4  7  4  1  1  5  1]
 [ 0  5  6  1  4  4  1 21]]
Accuracy: 0.6206, F1-score wieghted: 0.6051
In [24]:
dataframe = pd.DataFrame(confusion_matrix_gb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))

# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of gradient boosting"), plt.tight_layout()

plt.ylabel("True Class"), 
plt.xlabel("Predicted Class")
plt.show()

Точность и F1-score алгоритма градиентного бустинга из библиотеки sklearn.ensemble чуть ниже чем у нейронной сети. Для нейросети в этом наборе параметров получилось: Точность: 0.6842, Weighted F1-Score: 0.6811 Попробую использовать алгоритм xgboost (Extreme Gradient Boosting) из одноименной библиотеки.

In [25]:
import xgboost as xgb

xgb_model = xgb.XGBClassifier(n_estimators=100, learning_rate=0.3, max_depth=3, random_state=42)

# Train the model
xgb_model.fit(X_train, y_train)

# Make predictions on the test set
y_pred = xgb_model.predict(X_test)

# Calculate the confusion matrix
confusion_matrix_xgb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_xgb)

# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# print(f'F1 score: {f1:.3f}')

# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
# print(f'Accuracy: {accuracy:.3f}')
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix:
[[ 5  9  0  1  2  0  2  0]
 [ 4 91  3  1  6  0  1  4]
 [ 0 13 43  2 10  3  1  0]
 [ 1 10  4 38  4  1  0  3]
 [ 1  5  9  5 39  2  0  4]
 [ 0  0  5  0  7 23  0  0]
 [ 0  4  4  5  0  0  8  2]
 [ 0  5  4  0  5  2  0 26]]
Accuracy: 0.6393, F1-score wieghted: 0.6324
In [26]:
dataframe = pd.DataFrame(confusion_matrix_xgb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))

# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of xgboost"), plt.tight_layout()

plt.ylabel("True Class"), 
plt.xlabel("Predicted Class")
plt.show()

Результаты xgboost лучше чем у градиентного бустинга из sklearn.ensemble. Однако softmax в нейросети имеет не меньшую точность и F1 score, по крайней мере при этом наборе параметров.